Sidebar layouts in web interfaces allow your users to easily access
filters, settings and other inputs alongside the interactive features
they control. In the Getting Started with
dashboards article, we covered “page-level” sidebar layouts via the
page_sidebar() and page_navbar() functions. In
this article, we’ll explore the full range of sidebar layouts available
in bslib.
There are three main types of sidebar layouts: floating, filling, and multi-page/tab.
Use layout_sidebar() to create a sidebar layout that can
go anywhere on any page. This layout approach is great for visually
grouping together semantically related inputs and output(s). It can also
be paired with a card() to leverage
full_screen expansion, add a header/footer, and more.
layout_sidebar(
sidebar = sidebar("Sidebar"),
"Main contents"
)
layout_sidebar()card(
full_screen = TRUE,
card_header("Title"),
layout_sidebar(
sidebar = sidebar("Sidebar"),
"Main contents"
)
)
layout_sidebar() in card()In the Getting Started with dashboards
article, we saw how page_sidebar() yields a sidebar layout
that fills the page. Underneath the hood, page_sidebar() is
just a simple wrapper around page_fillable() and
layout_sidebar(). Understanding this unlocks the potential
to have (any number of) sidebar layouts within a filling layout.
page_fillable(
layout_sidebar(
sidebar = sidebar("Sidebar area"),
"Main area"
)
)
layout_sidebar() in page_fillable()For a multi-page (or multi-tab) layout, use the sidebar
argument of page_navbar() (or
navset_card_tab()). In this case, we get a sidebar that not
only fills the page, but that same sidebar remains visible on every
page/tab. Later on, we’ll explore how to put multiple, varied, layouts on different
pages; but also keep in mind, if it is actually desirable to have
the same sidebar on every page, it often helps to hide/show sidebar contents on certain
pages via conditionalPanel().
page_navbar(
sidebar = sidebar("Sidebar"),
nav_panel("Page 1", "Page 1 content"),
nav_panel("Page 2", "Page 2 content")
)
page_navbar()navset_card_tab(
sidebar = sidebar("Sidebar"),
nav_panel("Tab 1", "Tab 1 content"),
nav_panel("Tab 2", "Tab 2 content")
)
navset_card_tab()Now that we’ve enumerated bslib’s sidebar layout options, lets use some real data1 to create some real inputs and outputs, and explore some additional features of sidebar layouts.
In a Shiny app2, you’ll probably want to use inputs like
selectInput(), sliderInput(), etc., in the
sidebar, but because you’re reading this article in a static website,
we’ll use crosstalk
input widgets.
Throughout this section, we’ll make repeated use of the following
widgets from {plotly} and {leaflet}. The
details on how these widgets work alongside {crosstalk} to
create linked views isn’t important for understanding sidebar layouts,
but do keep in mind this will give us a list of filters and
plots (views of the diamonds dataset), as well
as map_filter and map_quakes (views of the
quakes dataset).
library(bslib)
library(shiny)
library(crosstalk)
library(plotly)
library(leaflet)
# Creates the "filter link" between the controls and plots
dat <- SharedData$new(dplyr::sample_n(diamonds, 1000))
# Sidebar elements (e.g., filter controls)
filters <- list(
filter_select("cut", "Cut", dat, ~cut),
filter_select("color", "Color", dat, ~color),
filter_select("clarity", "Clarity", dat, ~clarity)
)
# plotly visuals
plots <- list(
plot_ly(dat) |> add_histogram(x = ~price),
plot_ly(dat) |> add_histogram(x = ~carat),
plot_ly(dat) |> add_histogram(x = ~cut, color = ~clarity)
)
plots <- lapply(plots, \(x) config(x, displayModeBar = FALSE))
# map filter and visual
quake_dat <- SharedData$new(quakes)
map_filter <- filter_slider("mag", "Magnitude", quake_dat, ~mag)
map_quakes <- leaflet(quake_dat) |>
addTiles() |>
addCircleMarkers()
As we covered in Getting Started with
dashboards, the sidebar argument of
page_navbar() puts a sidebar on each page that
fills the window. However, sometimes it’s better that only particular
pages have such a sidebar layout. To acheive this, just provide a
layout_sidebar() as a “root” element of a
fillable page.
For example, let’s put a “page-level” sidebar on a page dedicated to
Earthquakes, and then put multiple sidebar layouts on a page dedicated
to Diamonds (one for each plot). In this case, we’ve only allowed the
Earthquakes page to be fillable since there are multiple
plots on the Diamonds page (you could also keep the Diamonds page
fillable an put a min_height on the cards to
prevent them from shrinking too much).
page_navbar(
title = "Sidebar demo",
fillable = "Earthquakes",
nav_panel("Earthquakes", sidebar_quakes),
nav_panel(
"Diamonds",
Map(
function(filter, plot) {
card(
full_screen = TRUE,
layout_sidebar(sidebar = filter, plot)
)
},
filters, plots
)
)
)
Just like page_navbar(), navset_card_tab()
also has a sidebar argument that puts the same
sidebar on each tab. The same approach (i.e., putting a
layout_sidebar() within each nav_panel()) can
be used to put different sidebars on different tabs.
Just like with cards,
when a filling layout isn’t enforcing the
size of the layout_sidebar(), it will allow it’s contents
to decide how big it should be. Thus, if there a large amount of
sidebar/main contents, consider specifying a height or
max_height via card() (as well as
full_screen = TRUE to reduce the need for scrolling).
page_fixed(
h1("Sidebar demo", class = "lead mt-3"),
card(
height = 400,
full_screen = TRUE,
layout_sidebar(sidebar = filters, plots)
),
card(
full_screen = TRUE,
layout_sidebar(sidebar = map_filter, map_quakes)
)
)
Although sidebars work just fine outside Shiny, using them in Shiny provides a few additional useful features.
Sometimes in a multiple page/tab setting, it’s useful to have a
sidebar on every page/tab, but changes it’s contents based on which
page/tab is active.3 Thanks to conditionalPanel(),
this can be done fairly easily in a Shiny app with
page_navbar() (or in
navset_card_tab()/navset_tab_pill()). The
trick is to provide an id to the page_navbar()
and then reference that id in the
conditionalPanel():
shinyApp(
page_navbar(
title = "Conditional sidebar",
id = "nav",
sidebar = sidebar(
conditionalPanel(
"input.nav === 'Page 1'",
"Page 1 sidebar"
),
conditionalPanel(
"input.nav === 'Page 2'",
"Page 2 sidebar"
)
),
nav_panel("Page 1", "Page 1 contents"),
nav_panel("Page 2", "Page 2 contents")
),
server = function(...) {
# no server logic required
}
)
To programmatically update (and/or re-actively read) the open/closed
state of a sidebar(), provide an id and
reference that id in your server code. Here we reference
use the id to programmatically open the sidebar on the 2nd
page.
library(shiny)
ui <- page_navbar(
title = "Sidebar updates",
id = "nav",
sidebar = sidebar(
id = "sidebar",
open = FALSE,
"Sidebar"
),
nav_panel("Page 1", "Sidebar closed. Go to Page 2 to open."),
nav_panel("Page 2", "Sidebar open. Go to Page 1 to close.")
)
server <- function(input, output) {
observe({
sidebar_toggle(
id = "sidebar",
open = input$nav == "Page 2"
)
})
}
shinyApp(ui, server)
All sidebars have special treatment for accordions. When an
accordion() appears directly within a
sidebar() (as an immediate child of the sidebar), the
accordion panels will render flush to the sidebar, providing a
convenient way to group multiple related input controls under a
collapsible section.
This example depends on objects from the setup code section.
accordion_filters <- accordion(
accordion_panel(
"Dropdowns", icon = bsicons::bs_icon("menu-app"),
!!!filters
),
accordion_panel(
"Numerical", icon = bsicons::bs_icon("sliders"),
filter_slider("depth", "Depth", dat, ~depth),
filter_slider("table", "Table", dat, ~table)
)
)
card(
card_header("Groups of diamond filters"),
layout_sidebar(
sidebar = accordion_filters,
plots[[1]]
)
)
In the above sections we’ve focused primarily on the variety of
interface layouts where sidebars can be used. Along the way, we’ve
touched on a few of the named arguments of sidebar() and
layout_sidebar() that are helpful for customizing the
styling and behavior of both the sidebar and main content areas.
However, there are a handful of other arguments to further customize the
look and feel if the sidebar layout.
Both sidebar() and layout_sidebar() allow
for a specific background color (via bg), which is applied
to the sidebar area and main content area respectively. When
bg is provided, bslib automatically provides a
high-contrast foreground color to ensure readability (but a
fg color may also be provided). Both functions also include
a class argument that works well with Bootstrap
utility classes and a style argument for
inline styles.
Be aware that in layout_sidebar(), bg,
class and style attributes are applied to the
main content area’s container and not the overall layout
container. To add additional classes to the layout container, use
htmltools::tagAppendAttributes(). Also note that
layout_sidebar() derives some of it’s default style from
Bootstrap CSS variables (e.g., --bs-card-border-color),
which enables theming at the component-level (theming via bs_theme() works
on the page-level).
The following example combines all of these concepts to create
sidebar with a dark background. Utility classes are used to make the
sidebar text monospace and bold, and we used
tagAppendAttributes() to tweak the border color of the
sidebar layout to match the sidebar background.
library(htmltools)
library(leaflet)
squake <- SharedData$new(quakes)
container <- layout_sidebar(
class = "p-0",
sidebar = sidebar(
title = "Earthquakes off Fiji",
bg = "#1E1E1E",
width = "35%",
class = "fw-bold font-monospace",
filter_slider("mag", "Magnitude", squake, ~mag)
),
leaflet(squake) |> addTiles() |> addCircleMarkers()
)
tagAppendAttributes(container, style = css("--bs-card-border-color" = "#1E1E1E"))
Our “real data” is just a 1,000 rows randomly sampled
from {ggplot2}’s diamonds data as well as
{leaflet}’s quakes.↩︎
In a Shiny app, we also recommend you add an
id to the sidebar() so that you can reactively
read/update whether the sidebar is open/closed.↩︎
If the controls depend on some other application state,
you’ll need to use uiOutput() to fill the contents of a
sidebar()).↩︎